#region Copyright & License
// Copyright 2008-2009 BUGCOLLECT.COM
// Author: Remus Rusanu
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//                 http://bugcollect.com/terms
//
#endregion

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.Net;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.XPath;

namespace bugcollect.open
{
  
    /// <summary>
    /// This class handles submitting reports to <a href="http://bugcollect.com">bugcollect.com</a>.
    /// </summary>
    /// <remarks>Reports are submited asynchronously. While there are still reports
    /// pending submission the <a cref="bugcollect.open.ReportingManager.WaitPendingDone">WaitPendingDone</a> event is not signaled. On application shutdown
    /// or appdomain unload the application should wait for this event to become signaled.</remarks>
    /// <example>
    /**
    <code escaped="true">
    class Program
    {
        static void Main(string[] args)
        {
            ReportingManager rm = new ReportingManager(
                Settings.Default.ApplicationName, 
                Settings.Default.ApplicationPassword);

            try
            {
                throw new ApplicationException("A problem was detected");
            }
            catch (Exception e)
            {
                rm.Submit("Developer notes", 
                    e, null, 
                    new ReportSubmissionResultHandler(CallbackReportCompleted));
            }
            rm.WaitPendingDone.WaitOne();
        }

        private static void CallbackReportCompleted(
            object sender,
            ReportSubmissionResultArgs args)
        {
            if (args.Exception != null)
            {
                Console.Error.WriteLine(args.Exception);
            }
            if (args.Result != null)
            {
                Console.WriteLine(args.Result.Status);
                using (TextReader reader = new StreamReader(args.Result.Response))
                {
                    Console.WriteLine(reader.ReadToEnd());
                }
            }
        }
    }
    </code>
     * */
    /// </example>
    public sealed class ReportingManager
    {
        #region Consts and Tags

        const string reportServerRoot = "http://bugcollect.com";
        const string reportServer = "http://bugcollect.com/rest/reports";

        const string authenticationDigest = "digest";
        const string methodPost = "POST";
        const string contentType = "text/xml";
        const string supressFile = "bugcollect.ignore";

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes the ReportingManager with the given credentials.
        /// </summary>
        /// <remarks>
        /// Do not use your own <a href="http://bugcollect.com">bugcollect.com</a> account credentials. 
        /// Create an application name and password from your Account page on <a href="http://bugcollect.com">bugcollect.com</a>
        /// and use them in your application. To prevent storing the application password hard coded
        /// in your application you can use the application configuration. To protect the application password 
        /// in the configuration file you can use an encrypted configuration section using
        /// <a href="http://msdn.microsoft.com/en-us/library/system.configuration.sectioninformation.protectsection(VS.80).aspx">SectionInformation.ProtectSection</a> method.
        /// </remarks>
        /// <param name="applicationName">Application name used for authentication with bugcollect.com.</param>
        /// <param name="applicationPassword">Application password used for authentication with bugcollect.com.</param>
        public ReportingManager(
            string applicationName,
            string applicationPassword)
        {
            _applicationName = applicationName;
            _callbackIdempotentTaskSubmit = new WaitCallback(IdempotentTaskSubmit);
            _callbackGetRequestStream = new AsyncCallback(CallbackGetRequestStream);
            _callbackGetResponse = new AsyncCallback(CallbackGetResponse);
            _callbackWriteRequest = new AsyncCallback(CallbackWriteRequest);
            SetCredentials(applicationName, applicationPassword);
            _pendingDoneEvent = new ManualResetEvent(true);
            _reportQueue = new Queue<ReportSubmissionState>();

            _appdomainUnhandledExceptionHandler = new UnhandledExceptionEventHandler(OnAppdomainUnhandledException);
            _threadExceptionEventHandler = new ThreadExceptionEventHandler(OnThreadException);
            _unhandledExceptionCallback = new ReportSubmissionResultHandler(UnhandledExceptionReportCallback);
        }

        #endregion

        #region Submit Supression

        private string _applicationName = null;
        private Dictionary<string, byte[]> _supressedReports = null;
        private bool _isApplicationSupressed = false;
        private DateTime _lastWriteTime = DateTime.MinValue;

        private void LoadSupressed()
        {

            string fileSupress = Path.Combine(
                Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "bugcollect"),
                supressFile);
            if (File.Exists(fileSupress))
            {
                // Check the file write time
                // If is newer than cache, reload file
                //
                FileInfo fi = new FileInfo(fileSupress);
                DateTime lastWriteTime = fi.LastWriteTimeUtc;

                if (lastWriteTime > _lastWriteTime)
                {
                    using (FileStream fs = new FileStream(
                        fileSupress, FileMode.Open, FileAccess.Read, FileShare.Read))
                    {
                        LoadSuppressedFromStream(fs, fi.LastWriteTimeUtc);
                    }
                }
            }
        }

        private void LoadSuppressedFromStream(FileStream fs, DateTime lastWriteTime)
        {
            Dictionary<string, byte[]> supressedReports = new Dictionary<string, byte[]>();
            bool isAppSuppressed = false;

            try
            {
                XPathDocument xdoc = null;
                XmlReaderSettings xrs = new XmlReaderSettings();
                xrs.CloseInput = false;
                using (XmlReader reader = XmlReader.Create(fs, xrs))
                {
                    xdoc = new XPathDocument(reader);
                }

                XPathNavigator xnavDoc = xdoc.CreateNavigator();
                XPathNavigator xnodeApp = xnavDoc.SelectSingleNode("//suppress/application[@name='" +
                    _applicationName + "']");
                if (null != xnodeApp)
                {
                    isAppSuppressed = true;
                }
                else
                {
                    XPathNodeIterator xnodesitReports = xnavDoc.Select("//suppress/report[@hash]");
                    foreach (XPathNavigator node in xnodesitReports)
                    {
                        string hash = node.GetAttribute("hash", "");
                        byte[] response = Convert.FromBase64String(node.Value);
                        supressedReports.Add(hash, response);
                    }
                }
            }
            catch (Exception e)
            {
                Debug.WriteLine(e);
            }


            lock (this)
            {
                if (lastWriteTime > _lastWriteTime)
                {
                    _lastWriteTime = lastWriteTime;
                    _supressedReports = supressedReports;
                    _isApplicationSupressed = isAppSuppressed;
                }
            }
        }


        private bool CheckSupressed(byte[] reportHash, out byte[] response)
        {
            response = null;
            try
            {
                LoadSupressed();

                Dictionary<string, byte[]> suppresedReports = null;
                lock (this)
                {
                    if (_isApplicationSupressed)
                    {
                        return false;

                    }
                    suppresedReports = _supressedReports;
                }
                if (null == suppresedReports)
                {
                    return false;
                }
                string reportHashBase64 = Convert.ToBase64String(reportHash);
                return suppresedReports.TryGetValue(reportHashBase64, out response);
          }
            catch (Exception e)
            {
                Debug.WriteLine(e);
            }
            return false;
        }

        private void AddIgnoredReport(byte[] reportHash, Stream response)
        {
            try
            {
                string pathSupress = Path.Combine(
                   Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
                   "bugcollect");
                if (false == Directory.Exists(pathSupress))
                {
                    Directory.CreateDirectory(pathSupress);
                }
                string fileSupress = Path.Combine(
                   pathSupress,
                   supressFile);
                using (FileStream fs = new FileStream(
                        fileSupress, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
                {

                    LoadSuppressedFromStream(fs, DateTime.UtcNow);

                    Dictionary<string, byte[]> suppressedReports = _supressedReports;
                   
                    string reportHashBase64 = Convert.ToBase64String(reportHash);

                    byte[] existingResult;
                    if (false == suppressedReports.TryGetValue(reportHashBase64, out existingResult))
                    {
                        fs.Seek(0, SeekOrigin.Begin);
                        XmlWriterSettings xms = new XmlWriterSettings();
                        xms.Encoding = Encoding.UTF8;

                        using (XmlWriter xmlWriter = XmlWriter.Create(fs))
                        {
                            xmlWriter.WriteStartElement("suppress");
                            foreach(string hash in suppressedReports.Keys)
                            {
                                xmlWriter.WriteStartElement("report");
                                xmlWriter.WriteAttributeString(
                                    "hash", hash);
                                byte[] hashResponse = suppressedReports[hash];
                                xmlWriter.WriteBase64(hashResponse, 0, hashResponse.Length);
                                xmlWriter.WriteEndElement();
                            }

                            xmlWriter.WriteStartElement("report");
                            xmlWriter.WriteAttributeString(
                                "hash", Convert.ToBase64String(reportHash));
                            
                            response.Seek(0, SeekOrigin.Begin);
                            byte[] buffer = new byte[999];
                            int read = response.Read(buffer, 0, buffer.Length);
                            while (read > 0)
                            {
                                xmlWriter.WriteBase64(buffer, 0, read);
                                read = response.Read(buffer, 0, buffer.Length);
                            }

                            xmlWriter.WriteEndElement(); // report
                            xmlWriter.WriteEndElement(); // suppress
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Debug.WriteLine(e);
            }
        }

        #endregion

        #region Count Pending and Signaling
        private volatile int _countPending;
        private ManualResetEvent _pendingDoneEvent;

        private void IncrementPending()
        {
            // The lock is needed to protect the race when
            // T1 decrements to 0 and T2 increments to 1
            // 
            lock (_pendingDoneEvent)
            {
                int countPending = ++_countPending;
                Debug.Assert(countPending > 0);
                if (1 == countPending)
                {
                    _pendingDoneEvent.Reset();
                }
            }
        }

        private void DecrementPending()
        {
            lock (_pendingDoneEvent)
            {
                int countPending = --_countPending;
                Debug.Assert(countPending >= 0);
                if (0 == countPending)
                {
                    _pendingDoneEvent.Set();
                }
            }
        }

        /// <summary>
        /// The wait handle is signaled when there are no more
        /// reports pending submission. 
        /// </summary>
        /// <remarks>
        /// Reports are submitted by the <a href="T_bugcollect_ReportingManager.htm">ReportingManager</a> 
        /// asynchronously. Internally the ReportingManager stores the reports pending
        /// submission in a queue. When the submission of the last report in the queue is
        /// complete the ReportingManager will signal the WaitPendingDone event.
        /// Applications should wait for this event to become signaled before 
        /// exiting the process or shutting down the appdomain, otherwise they risk
        /// aborting reports pending submission.
        /// </remarks>
        public WaitHandle WaitPendingDone
        {
            get { return _pendingDoneEvent; }
        }
        #endregion

        #region Credentials

        private CredentialCache _credentials;

        private void SetCredentials(
            string userName,
            string password)
        {
            NetworkCredential networkCredential = new NetworkCredential(
                userName, password);

            CredentialCache credentialCache = new CredentialCache();
            credentialCache.Add(new Uri(reportServerRoot),
                authenticationDigest,
                networkCredential);

            _credentials = credentialCache;
        }

        #endregion

        #region Report queue

        private enum EIdempotentTaskState : int
        {
            Idle,
            Launched,
            Running,
        }

        private Queue<ReportSubmissionState> _reportQueue;

        /// <summary>
        /// Places a report in the pending queue and launches the
        /// idempotent task for actual submission. Reports are not
        /// submitted on caller three to avoid DNS calls blocking
        /// </summary>
        /// <param name="handler"></param>
        private void EnqueueReportForSubmission(ReportSubmissionState handler)
        {
            lock (_reportQueue)
            {
                IncrementPending();
                try
                {
                    _reportQueue.Enqueue(handler);
                }
                catch (Exception)
                {
                    DecrementPending();
                    throw;
                }
            }
            LaunchIdempotentTaskSubmit();
        }

        /// <summary>
        /// Launches the idempotent task for submitting the reports
        /// Multiple threads can call this, but only one will launch 
        /// the idempotent task. Task is not launhed if is 
        /// already running.
        /// </summary>
        private void LaunchIdempotentTaskSubmit()
        {
            if ((int)EIdempotentTaskState.Idle == _isRunningIdempotentTask)
            {
                Interlocked.CompareExchange(
                    ref _isRunningIdempotentTask,
                    (int)EIdempotentTaskState.Launched,
                    (int)EIdempotentTaskState.Idle);
                try
                {
                    ThreadPool.QueueUserWorkItem(_callbackIdempotentTaskSubmit);
                }
                catch (Exception)
                {
                    Interlocked.CompareExchange(
                        ref _isRunningIdempotentTask,
                        (int)EIdempotentTaskState.Idle,
                        (int)EIdempotentTaskState.Launched);
                    throw;
                }
            }
        }

        private WaitCallback _callbackIdempotentTaskSubmit;

        private int _isRunningIdempotentTask;

        /// <summary>
        /// Handles the actual submission of reports to 
        /// the collection site. Prepares a WebRequest
        /// and launches an asynchronous I/O operation
        /// </summary>
        /// <param name="state">Ignored parameter</param>
        private void IdempotentTaskSubmit(object state)
        {
            int wasRunning = Interlocked.CompareExchange(
                ref _isRunningIdempotentTask,
                (int)EIdempotentTaskState.Running,
                (int)EIdempotentTaskState.Launched);
            if ((int)EIdempotentTaskState.Launched == wasRunning)
            {
                ReportSubmissionState handler = null;
                try
                {
                    do
                    {
                        handler = null;

                        lock (_reportQueue)
                        {
                            if (_reportQueue.Count > 0)
                            {
                                handler = _reportQueue.Dequeue();
                            }
                            else
                            {
                                Interlocked.Exchange(
                                    ref _isRunningIdempotentTask,
                                    (int)EIdempotentTaskState.Idle);
                                break;
                            }
                        }
                        Debug.Assert(null != handler);
                        try
                        {
                            byte[] suppressedResponse = null;
                            if (false == CheckSupressed(handler.ReportHash, out suppressedResponse))
                            {
                                HttpWebRequest request = CreateRequest();
                                request.ContentLength = handler.Size;
                                handler.Request = request;

                                IncrementPending();
                                try
                                {
                                    request.BeginGetRequestStream(
                                        _callbackGetRequestStream, handler);
                                }
                                catch (Exception)
                                {
                                    DecrementPending();
                                    throw;
                                }
                            }
                            else
                            {
                                Result suppressedResult = Result.CreateSuppressedResult(suppressedResponse);
                                handler.PerformCallback(this, suppressedResult, null);
                            }
                        }
                        finally
                        {
                            // Decrement for the Dequeue operation
                            DecrementPending();
                        }
                    } while (true);
                }
                catch (Exception ex)
                {
                    Interlocked.Exchange(ref _isRunningIdempotentTask, (int)EIdempotentTaskState.Idle);
                    Debug.WriteLine(ex);
                    // resubmit itself
                    LaunchIdempotentTaskSubmit();

                    if (null != handler)
                    {
                        handler.PerformCallback(this, null, ex);
                    }
                }
            }
        }


        #endregion

        #region HTTP call handling

        private AsyncCallback _callbackGetRequestStream;
        private AsyncCallback _callbackGetResponse;
        private AsyncCallback _callbackWriteRequest;

        private void CallbackGetRequestStream(IAsyncResult ar)
        {
            ReportSubmissionState handler = null;
            try
            {
                Debug.Assert(ar.AsyncState is ReportSubmissionState);
                handler = (ReportSubmissionState)ar.AsyncState;

                handler.RequestStream = handler.Request.EndGetRequestStream(ar);

                int read = handler.ReportStream.Read(handler.Buffer, 0, handler.Buffer.GetLength(0));
                handler.TransferOffset = read;

                // the request stream write is actual wire send so we do it async
                handler.RequestStream.BeginWrite(
                    handler.Buffer, 0, read, _callbackWriteRequest, handler);
            }
            catch (Exception ex)
            {
                if (null != handler.RequestStream)
                {
                    handler.RequestStream.Close();
                }
                handler.PerformCallback(this, null, ex);
                DecrementPending();
            }
        }

        private void CallbackWriteRequest(IAsyncResult ar)
        {
            ReportSubmissionState handler = null;
            try
            {
                Debug.Assert(ar.AsyncState is ReportSubmissionState);
                handler = (ReportSubmissionState)ar.AsyncState;
                handler.RequestStream.EndWrite(ar);

                if (handler.TransferOffset < handler.Size)
                {
                    // Send a new chunck
                    int read = handler.ReportStream.Read(
                        handler.Buffer,
                        handler.TransferOffset,
                        handler.Buffer.GetLength(0));
                    handler.TransferOffset += read;

                    handler.RequestStream.BeginWrite(
                        handler.Buffer, 0, read, _callbackWriteRequest, handler);
                }
                else
                {
                    // done, end the request
                    handler.RequestStream.Close();
                    handler.Request.BeginGetResponse(_callbackGetResponse, handler);
                }
            }
            catch (Exception ex)
            {
                handler.RequestStream.Close();
                handler.PerformCallback(this, null, ex);
                DecrementPending();
            }
        }

        private void CallbackGetResponse(IAsyncResult ar)
        {
            ReportSubmissionState handler = null;
            try
            {
                Debug.Assert(ar.AsyncState is ReportSubmissionState);
                handler = (ReportSubmissionState)ar.AsyncState;
                Result result = null;
                WebResponse response = null;
                WebException webException = null;
                try
                {
                    response = handler.Request.EndGetResponse(ar);
                }
                catch(WebException wex)
                {
                    if (null == wex.Response)
                    {
                        throw;
                    }
                    // We're going to try and interpret the payload, if possible
                    //
                    webException = wex;
                    response = webException.Response;
                }
                using(response)
                {
                    try
                    {
                        using (Stream responseStream = response.GetResponseStream())
                        {
                            using (XmlReader rdr = XmlReader.Create(responseStream))
                            {
                                if (false == Result.ParseXML(rdr, out result))
                                {
                                    throw new ApplicationException("Failed to parse the XML response");
                                }
                            }
                        }
                    }
                    catch (Exception)
                    {
                        // Throw the original Web exception, if set
                        if (null != webException)
                        {
                            throw webException;
                        }
                        // Otherwise rethrow the current exception
                        throw;
                    }

                    Debug.Assert(null != result);
                    if (result.Status == EResultStatus.IGNORED)
                    {
                        AddIgnoredReport(
                            handler.ReportHash,
                            result.Response);
                        result.Response.Seek(0, SeekOrigin.Begin);
                    }

                    handler.PerformCallback(this, result, null);
                    handler = null;
                }
            }
            catch (Exception ex)
            {
                if (null != handler)
                {
                    handler.PerformCallback(this, null, ex);
                }
                else
                {
                    Debug.WriteLine(ex);
                }
            }
            finally
            {
                DecrementPending();
            }
        }

        private HttpWebRequest CreateRequest()
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(reportServer);
            request.Method = methodPost;

            CredentialCache credentials = _credentials;
            Debug.Assert(null != credentials);
            request.Credentials = credentials;

            request.Accept = contentType;
            request.ContentType = contentType;

            return request;
        }

        #endregion

        #region Report submitting public APIs

        /// <summary>
        /// Submits a report for upload to <a href="http://bugcollect.com">bugcollect.com</a>
        /// The report is added to an submission queue and this
        /// method returns imeadetly. When the report submission
        /// is complete the provided callback is invoked with the
        /// submission result.
        /// <seealso cref="ReportingManager.Submit"/>
        /// <threadsafety>
        /// This method can be safely called from multiple threads.
        /// </threadsafety>
        /// </summary>
        /// <remarks>When the report submission is complete the provided callback will be invoked.</remarks>
        /// <param name="report">The report to be uploaded</param>
        /// <param name="userState">An arbitrary object that is passed back when invoking the callback.</param>
        /// <param name="callback">A callback to be invoked when the report submission is complete.</param>
        public void SubmitReport(
            Report report,
            object userState,
            ReportSubmissionResultHandler callback)
        {
            byte[] bucketHash = report.ComputeHash();

            ReportSubmissionState state = new ReportSubmissionState(
                report, userState, callback);
            EnqueueReportForSubmission(state);
        }

        /// <summary>
        /// Creates a new report and submits it for upload
        /// to <a href="http://bugcollect.com">bugcollect.com</a>. 
        /// </summary>
        /// <remarks>When the report submission is complete the provided callback will be invoked.</remarks>
        /// <seealso cref="ReportingManager.SubmitReport"/>
        /// <threadsafety>
        /// This method can be safely called from multiple threads.
        /// </threadsafety>
        /// <param name="notes">Notes associated with the report.</param>
        /// <param name="e">Exception object related to the report.</param>
        /// <param name="userState">An arbitrary object that is passed back when invoking the callback.</param>
        /// <param name="callback">A callback to be invoked when the report submission is complete.</param>
        public void Submit(
            string notes,
            Exception e,
            object userState,
            ReportSubmissionResultHandler callback)
        {
            Report report = new Report(notes, e, Assembly.GetCallingAssembly());
            SubmitReport(report, userState, callback);
        }

        #endregion

        #region AppDomain and Application event handlers

        private void OnAppdomainUnhandledException(object sender, UnhandledExceptionEventArgs args)
        {
            if (args.ExceptionObject is Exception)
            {
                Submit(null, (Exception)args.ExceptionObject, null, _unhandledExceptionCallback);
            }
        }

        private UnhandledExceptionEventHandler _appdomainUnhandledExceptionHandler;


        /// <summary>
        /// An handler that can be attached to the Appdomain.UnhandledException event.
        /// If attached thenit will submit a report to <a href="http://bugcollect.com">bugcollect.com</a> 
        /// for all unhandled exceptions in the appdomain.
        /// </summary>
        /// <remarks>
        /// This handler has to be attached to the current appdomain UnhandledException event
        /// at application start, as early as possible.
        /// </remarks>
        /// <seealso cref="bugcollect.ReportingManager.ThreadExceptionHandler"/>
        /// <seealso cref="bugcollect.ReportingManager.UnhandledExceptionResult"/>
        public UnhandledExceptionEventHandler AppdomainUnhandledExceptionHandler
        {
            get { return _appdomainUnhandledExceptionHandler; }
        }

        private void OnThreadException(object sender, ThreadExceptionEventArgs args)
        {
            Submit(null, args.Exception, null, _unhandledExceptionCallback);
        }

        private ThreadExceptionEventHandler _threadExceptionEventHandler;

        /// <summary>
        /// A handler that can be attached to the Application.ThreadException event.
        /// This handler will submit all unhandled exceptions in the user interface thread 
        /// to bugcollect.com.
        /// </summary>
        /// <remarks>
        /// The Application.ThreadException event is not raised by default. To enable this event
        /// your application must first call the Application.SetUnhandledExceptionMode method,
        /// or overwrite 
        /// </remarks>
        /// <example>
        /**
        <code>
        static class Program
        {
            [STAThread]
            static void Main()
            {
                ReportingManager rm = new ReportingManager(
                     Settings.Default.ApplicationName,
                     Settings.Default.ApplicationPassword);

                try
                {
                    Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
                    Application.ThreadException += rm.ThreadExceptionEventHandler;

                    Application.EnableVisualStyles();
                    Application.SetCompatibleTextRenderingDefault(false);
                    Application.Run(new ApplicationForm());
                }
                finally
                {
                    rm.WaitPendingDone.WaitOne();
                }
            }
        }
        </code>
        */
        /// </example>
        public ThreadExceptionEventHandler ThreadExceptionHandler
        {
            get { return _threadExceptionEventHandler; }
        }

        private ReportSubmissionResultHandler _unhandledExceptionCallback;
        private void UnhandledExceptionReportCallback(
            object sender,
            ReportSubmissionResultArgs args)
        {
            if (null != UnhandledExceptionResult)
            {
                UnhandledExceptionResult(this, args);
            }
        }

        /// <summary>
        /// This event is raised on completion of the submission of an unhandled exception.
        /// </summary>
        /// <remarks>
        /// To submit unhandled exceptions the ReportingManager.ThreadExceptionHandler must be attached to 
        /// the Application.ThreadException event, or the the ReportingManager.AppdomainUnhandledExceptionHandler
        /// must be attached to the Appdomain.UnhandledException event, or both.
        /// </remarks>
        /// <seealso cref="bugcollect.ReportingManager.ThreadExceptionHandler"/>
        /// <seealso cref="bugcollect.ReportingManager.AppdomainUnhandledExceptionHandler"/>
        public event ReportSubmissionResultHandler UnhandledExceptionResult;

        #endregion
    }
}
